﻿using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
using System.Data;
using System.Transactions;
using CoreEx.Common.Cache;
using CoreEx.Common.Data;
using CoreEx.Common.Proxy;
using CoreEx.DynamicInvoker;
using LinFu.IoC.Configuration;
using LinFu.IoC.Interfaces;
using LinFu.IoC;

namespace CoreEx.Database.Client.Implementation
{
    /// <summary>
    /// Returns a list of <see cref="IDataParameter"/> instances for a stored procedure.
    /// </summary>    
    [Implements(typeof(IParameterExtractor))]
    public class ParameterExtractor : IParameterExtractor,IInitialize
    {
        private ILazyCache<SurrogateKey, IEnumerable<IDataParameter>> _cache;
        
        private IServiceContainer _serviceContainer;
        private readonly string _connectionName;

        private readonly object _syncObject = new object();

        /// <summary>
        /// Initializes a new instance of the <see cref="ParameterExtractor"/> class.
        /// </summary>
        /// <param name="connectionName">The name of the configured connection.</param>
        public ParameterExtractor(string connectionName)
        {
            _connectionName = connectionName;
        }

        /// <summary>
        /// Returns list that contains all parameters for the stored procedure as
        /// specified by <paramref name="procedureName"/>
        /// </summary>
        /// <param name="procedureName">The name of the stored procedure</param>
        /// <returns>A cached list of <see cref="IDbDataParameter"/> instances.</returns>
        public IEnumerable<IDataParameter> GetParameters(string procedureName)
        {
            lock (_syncObject)
            {
                var key = CreateKey(procedureName);
                var cachedParameters = _cache.GetValue(key, s => DeriveParameters(procedureName));
                var parameters = cachedParameters.Cast<ICloneable>().Select(p => (IDataParameter) p.Clone());
                return parameters;
            }
        }

        /// <summary>
        /// Derives the parameters from a stored procedure.
        /// </summary>
        /// <param name="procedureName">The name of the procedure for which to derive its parameters</param>
        /// <returns>A list of <see cref="IDbDataParameter"/> instances.</returns>
        private IEnumerable<IDataParameter> DeriveParameters(string procedureName)
        {

                var dbConnection = _serviceContainer.GetService<IDbConnection>(_connectionName);
            
                var providerFactory = GetProviderFactory();
                var commandBuilder = providerFactory.CreateCommandBuilder();                
                var actualCommand = (IDbCommand) ((IActualTarget) dbConnection.CreateCommand()).Instance;
                actualCommand.CommandType = CommandType.StoredProcedure;
                actualCommand.CommandText = procedureName;
                commandBuilder.GetType().DynamicInvoke("DeriveParameters", new[] {actualCommand});
                if (Transaction.Current == null)
                    dbConnection.Close();
                return actualCommand.Parameters.Cast<IDataParameter>();
            
        }
        
        /// <summary>
        /// Gets a <see cref="DbProviderFactory"/> based on the configured connection name.
        /// </summary>
        /// <returns><see cref="DbProviderFactory"/></returns>
        private DbProviderFactory GetProviderFactory()
        {
            var dbConnectionInfo = _serviceContainer.GetService<DbConnectionInfo>(_connectionName);
            return DbProviderFactories.GetFactory(dbConnectionInfo.ProviderName);
        }

        /// <summary>
        /// Creates a <see cref="SurrogateKey"/> based on the configured connection name and the procedure name.
        /// </summary>
        /// <param name="procedureName">The name of the stored procedure.</param>
        /// <returns><see cref="SurrogateKey"/></returns>
        private SurrogateKey CreateKey(string procedureName)
        {
            return new SurrogateKey(new object[]{_connectionName,procedureName});
        }

        /// <summary>
        /// Initializes this service instance.
        /// </summary>        
        public void Initialize(IServiceContainer source)
        {
            _cache = source.GetService<ILazyCache<SurrogateKey, IEnumerable<IDataParameter>>>();                                    
            _serviceContainer = source;
        }
    }
}
